from pathlib import PurePath
from datetime import timedelta
from datetime import datetime as dt
import shutil
from colorama import Fore, Back, init
init(autoreset=True)
import os, sys
# sys.path.append(os.path.dirname(os.path.dirname(__file__)))
import traceback
from natsort import natsorted 

from hm.common import *
from hm.hos import *
from hm.htime import Str2Time

from report import HmDocx, HmTable
from ODBC_report import logger, NotebookDocx, REGEX_DICT, ReSearchWrapper, \
    CR, FIX, Serializer

from docx.table import Table
from docx.text.paragraph import Paragraph




class ExecutionTable(HmTable):
    ATTR = {
        "TestCaseNO":{"row":0, "regex":'^Test Case NO', "col_offset":1},
        "Tester":{"row":0, "regex":'^Tester:', "col_offset":1},
        "Tester_hw":{"row":0, "regex":'^Tester:', "col_offset":1},
        "Tester_sw":{"row":0, "regex":'^Tester:', "col_offset":0},
        "SystemID":{"regex":'^System ID:|^Analyzer ID:', "col_offset":1},
        "DateRangeTested":{"row":1, "regex":'^Date.*Tested', "col_offset":1},
        "TestCaseVersion":{"row":2, "regex":'^Test Case Version', "col_offset":1},
        "SoftwareVersion":{"row":2, "regex":'^Software Version', "col_offset":1},
        "SystemPlatform":{"row":3, "regex":'^System Platform', "col_offset":1}
    }
    
    def __init__(self, table, parent):
        '''
            因为变量名必须和summary的一样...这里的名字就不能随便改了...
        '''
        super().__init__(table, parent)
        logger.info("********** [初始化执行报告表格] **********")
        self.re_search = ReSearchWrapper("ExecutionTable")
        self.re_dict = REGEX_DICT["ExecutionTable"]
        # SystemID_ix = self.locate_cell("Tester", row=0)[1]-1
        # tester_ix = self.locate_cell("Test Review", row=0)[1]-1
        # self.TestCaseNO = self.rows[0].cells[SystemID_ix].text.strip()
        # self.Tester = self.rows[0].cells[tester_ix].text.strip()
        
        # 特殊处理 
        # if '--sw' in sys.argv:
        #     if self.Tester_sw:
                # 不能这样玩，如果跑多个报告，那么后面的报告的ATTR都变了
                # self.ATTR["Tester"]["col_offset"] = 0
                # del self.Tester # 不操作单元格（见report.py __setattr__方法）
                # self.Tester = self.Tester_sw
                
        del self.Tester # 不操作单元格（见report.py __setattr__方法）
        if self.Tester_sw: # SW优先
            self.Tester = self.Tester_sw
            self.ATTR["Tester"] = self.ATTR["Tester_sw"]
        elif self.Tester_hw:
            self.Tester = self.Tester_hw
            self.ATTR["Tester"] = self.ATTR["Tester_hw"]
        else:
            self.Tester = ''
        
       
        # self.SystemID = self.rows[1].cells[SystemID_ix].text.strip()
        # self.DateRangeTested = self.rows[1].cells[tester_ix].text.strip()
        # self.TestCaseVersion = self.rows[2].cells[SystemID_ix].text.strip()
        # self.SoftwareVersion = self.rows[2].cells[tester_ix].text.strip()
        # match = self.re_search("SoftwareVersion", self.SoftwareVersion)
        # if match:
        #     self.SoftwareVersion = match.group(1)
        # self.SystemPlatform = self.rows[3].cells[tester_ix].text.strip()
        # assay_data_disk
        self.assay_data_disk = None
        if self.SystemPlatform:
            self.assay_data_disk = self.re_search("ADD", self.SystemPlatform)
            if self.assay_data_disk:
                self.assay_data_disk = self.assay_data_disk.group(1).strip()
        self.start_step_row, self.end_step_row = self.start_end_step_row()
        # self.start_step_ix, self.end_step_ix = self.start_step_row._index, self.end_step_row._index
        self.start_step_ix, self.end_step_ix = self.start_step_row.index, self.end_step_row.index # 20240619 用index代替_index，适配SW报告格式
        self.actual_result_index = self.locate_cell(r"Actual.+?Result", row=self.start_step_ix-1)[1] 
        # Status 所在的列索引
        status_location = self.locate_cell(r"status", row=self.start_step_ix-1) # 20250124
        if not status_location:
            status_location = self.locate_cell(r"Pass.*?Fail", row=self.start_step_ix-1, flags=re.DOTALL) # 20250124
        # self.status_index = self.locate_cell(r"status.+?pass", row=self.start_step_ix-1)[1] 
        self.status_index = status_location[1] 
        self.record, self.Conclusion, self.equipment, self.TimeSpent = None, None, None, None
        if self.end_step_ix+3<len(self.rows):
            self.record = self.rows[self.end_step_ix+1].cells[0].text.strip()
            self.Conclusion = self.rows[self.end_step_ix+2].cells[-1].text.strip()
            self.equipment = self.rows[self.end_step_ix+3].cells[-1].text.strip()
            # 测试花费时间
            self.TimeSpent = self.re_search("TimeSpent", self.record)
            if self.TimeSpent:
                unit = ' Hours' if self.TimeSpent.group(1)!='1' else ' Hour' # 单复数格式区分
                self.TimeSpent = self.TimeSpent.group(1)+ unit # 加Hours是为了和Sumary表格统一
        
        self.init_dir()
        
        # 需求描述行
        self.Requirements = {} # {需求:需求描述}
        ix = self.locate_cell("Requirements", col=0)
        if ix:
            ix = ix[0]
            string = self.rows[ix].cells[1].text
            # 返回所有匹配内容而不是只返回子组
            match = self.re_search("requirements", string, find_iter=True) 
            for m in match:
                if m:
                    req = re.sub(r"(^[ :-]*)|(\n.{0,4})$", '', m.group(2))
                    self.Requirements[m.group(1)] = req

        logger.info(f"""表头信息：
                        TestCaseNO：{self.TestCaseNO};
                        Tester:{self.Tester};
                        Tester_sw:{self.Tester_sw};
                        SystemID:{self.SystemID};
                        DateRangeTested:{self.DateRangeTested};
                        TestCaseVersion:{self.TestCaseVersion};
                        SoftwareVersion:{self.SoftwareVersion};
                        SystemPlatform:{self.SystemPlatform};
                        assay_data_disk:{self.assay_data_disk};
                        self.start_step_ix:{self.start_step_ix};
                        self.end_step_ix:{self.end_step_ix};
                        record:{self.record};
                        Conclusion:{self.Conclusion};
                        equipment:{self.equipment};
                        TimeSpent:{self.TimeSpent};
                        Requirements:{self.Requirements};
                        """)
    
        # *********** 以下变量调用check方法后生效 **********
        # 全部步骤行
        self.all_step_rows = self.rows[self.start_step_ix:self.end_step_ix+1] 
        # 有效步骤（序号是数字，非合并行）行
        self.valid_step_rows = []
        # 每一步测试信息:
        # [{"step_num": None, "ActualResult":None, "Status":None, 
        #   "Redmines":[], "TEST_CASE_EDIT":None}, {...}, {...}, ...]
        self.steps_info = [] 
        # 解决self.steps_info保存的step_num与元素索引未必一致问题
        # self.steps_info_ix的键是步骤序号，值是此步骤在self.steps_info中的索引
        self.steps_info_ix = {}
        self.skip_step = [] # 由于某些原因跳过的步骤，如重测报告跳过not executed
        # 执行结果统计
        self.TotalSteps = 0
        self.StepsPassed = 0
        self.StepsFailed = 0
        self.StepsIncomplete = 0
        self.StepsNotExecuted = 0
        # 测试开始和结束日期（字符串）
        self.StartDate = None
        self.EndDate = None
        # 测试开始和结束日期（datetime对象）
        self._max_time = None
        self._min_time = None
        self.is_pass = True # 此用例是pass还是fail

    
        
        
    def init_dir(self):
        self.path = self.parent.path
        self.notebook_dir = self.parent.notebook_dir
        self.evidence_dir = self.parent.evidence_dir
        logger.info(f"""
                    当前文件路径{self.path}，
                    notbook_dir：{self.notebook_dir}, 
                    evidence_dir:{self.evidence_dir}""")
        self.steps_dir = None # stepxx文件夹所在的目录
        if self.evidence_dir:
            retry_dirs = []
            file_caseNo = self.parent.case_name
            # print(self.evidence_dir, file_caseNo, self.SystemID)
            if file_caseNo:
                # 优先以文件名查找路径
                dir3 = os.path.join(self.evidence_dir, file_caseNo, self.SystemID)   
                dir4 = os.path.join(self.evidence_dir, self.SystemID, file_caseNo)
                dir5 = os.path.join(self.evidence_dir, self.SystemID)
                dir6 = os.path.join(self.evidence_dir, file_caseNo)
                
                retry_dirs += [dir3, dir4, dir5, dir6]
            # 不同的人证据路径可能不一样，这里分为两种路径
            dir1 = os.path.join(self.evidence_dir, self.TestCaseNO, self.SystemID)   
            dir2 = os.path.join(self.evidence_dir, self.SystemID, self.TestCaseNO)
            dir3 = os.path.join(self.evidence_dir, self.TestCaseNO)
            
            retry_dirs += [dir1, dir2, dir3]
            
            self.steps_dir = next((d for d in retry_dirs if os.path.exists(d)), None)
            logger.info(f"retry_dirs：{retry_dirs}；steps_dir：{self.steps_dir}")

    def start_end_step_row(self):
        '''
        # 获取第1步和最后1步执行步骤的行索引
        # 注意：如果获取不到索引会导致一系列错误
        '''
        start, end = None, None
        for i, row in enumerate(self.rows):
            cells = row.cells
            if start:
                is_num = re.search(r"\d+", cells[0].text) or cells[0].text==''
                if have_word("Test Record", cells[0].text) or not is_num:
                    end = self.rows[i-1]
                    break
            elif have_word("step", cells[0].text) and self.locate_cell("Action", row=i) \
                and self.locate_cell("Expected[ -_]Result", row=i):
                if not start:
                    start = self.rows[i+1]
                    # continue
        if start is None: # or end is None:
            raise Exception(f"{Fore.RED}找不到测试步骤所在的行，请检查“测试步骤标题行”和“Test Record行”是否存在！")
        if end is None:
            end = self.rows[-1]
        if not hasattr(start, "index"):
            start.index, end.index = start._index, end._index
        logger.info(f"开始和结束步骤：{start.index} {end.index}")
        return start, end
    
    def parse_test_time(self):
        '''
            检查时间格式
            转换时间为datetime对象
        '''
        times = Str2Time(self.DateRangeTested).tt
        if not times:
            yield CR.error(f"Date Range Tested 找不到日期")
        else:
            for i,t in enumerate(times):
                if i==0:
                    self._max_time, self._min_time = t.dt, t.dt
                    self.StartDate, self.EndDate = t.raw_str, t.raw_str
                else:
                    if t.dt>self._max_time:
                        self._max_time = t.dt
                        self.EndDate = t.raw_str
                    if t.dt<self._min_time:
                        self._min_time = t.dt
                        self.StartDate = t.raw_str
                fmt_str = dt.strftime(t.dt, format='%d-%b-%Y')
                if fmt_str!=t.raw_str:
                    yield CR.error(f"Date Range Tested {t.raw_str}建议改为{fmt_str}")


    def check_title(self):
        d = {"Test Case NO":self.TestCaseNO, 
            "Tester":self.Tester, 
            "System ID": self.SystemID,
            "Date Range Tested":self.DateRangeTested, 
            "Test Case Version":self.TestCaseVersion,
            "Software Version": self.SoftwareVersion, 
            "System Platform/Configuration": self.SystemPlatform
        }
        for key in d:
            if not d[key]:
                yield CR.error(f"{key} 未填写")
        for e in self.parse_test_time():
            yield e
        
    def filter_step_row(self):
        '''
            检查步骤序号
        '''
        count = 0
        for i, row in enumerate(self.all_step_rows):
            cells0 = row.cells[0]
            step_text = cells0.text.strip()
            step_text_re = re.search(r'^(\d+)\.?$', step_text)
            if step_text_re:
                step_text = step_text_re.group(1)
                cells0.text = step_text
            
            if step_text=='': 
                if "w:numPr" in cells0._element.xml: # 是自动编号
                    step_text = str(count)

            err = f"第{self.start_step_ix+i}行，步骤序号“{step_text}”"
            if not step_text.isdigit(): 
                yield CR.error(f"{err} 不是数字")
                continue
            if i>0 and self.is_merge(cells0, self.all_step_rows[i-1].cells[0], 'y'):
                # 当前行和上一行是合并的单元格
                continue
            if int(step_text)-count>1:
                yield CR.error(f"{err}不按单步递增，应为{count+1}")
            count += 1
            self.valid_step_rows.append(row)
        logger.info(f"self.all_step_rows{self.all_step_rows}")
        logger.info(f"self.valid_step_rows{self.valid_step_rows}")

    def skip_retest_ne(self, Status):
        """
            重测的报告跳过Not executed
        """
        if self.parent.is_retested:
            if have_word("(Not execut(ed)?)|NE", Status):
                self.StepsNotExecuted += 1
                self.TotalSteps += 1
                return True
        return False
    
    def get_step_info(self):
        '''
            将有效步骤行信息存入字典
            ix: self.step_info列表的索引
        '''
        assert self.valid_step_rows,'找不到有效步骤行，可能需要先运行self.filter_step_row'
        # todo可能要加个异常捕获
        ix = 0
        for count,row in enumerate(self.valid_step_rows):
            cells = row.cells
            info = {"TEST_CASE_EDIT":None} 
            info['row'] = row
            info['step_num'] = cells[0].text.strip()
            if info['step_num']=='': # 自动编号？
                info['step_num'] = str(count+1)

            info["ActualResult"] = cells[self.actual_result_index].text.strip()
            info["Status"] = cells[self.status_index].text.strip()
            info["Action"] = cells[1].text.strip()
            info['ExpectedResult'] = cells[2].text.strip()
            if self.skip_retest_ne(info["Status"]): # 重测报告跳过not executed步骤
                self.skip_step.append(info['step_num'])
                continue
            info["Redmines"] = self.re_search("redmines", info["ActualResult"], find_all=True)
            if have_word("test case edit", info["ActualResult"]):
                info["TEST_CASE_EDIT"] = "..."
            # todo需求可能长这样：E10307-SSRD-442 – The reflectometer shall sense which filter 
            # 也可能长这样：ODBC-SAHA-SSRD-0003 - The LVCC SAHA shall support all the on-board 
            info['requirements'] = re.findall(r"(?:[\w]{3,6}-){2,3}\d{3,4}", cells[-1].text)
            if not have_word('pass', info['Status']):
                info['is_pass'] = False
            else:
                info['is_pass'] = True
            self.steps_info.append(info)
            self.steps_info_ix[info['step_num']] = ix # 根据step_num可访问它在self.step_info中的索引
            ix += 1
        logger.info(f"step_info字典：{self.steps_info}\n步骤索引steps_info_ix：{self.steps_info_ix}")
    
    def count_pass_fail(self):
        '''
            统计测试结果数量
        '''
        assert self.steps_info,"需要先运行self.get_step_info"
        self.TotalSteps += len(self.steps_info)
        for step in self.steps_info:
            status = step["Status"]
            # if have_word('fail', status):
            if self.re_search("Failed", status):
                self.StepsFailed += 1
            # elif have_word('Pass', status):
            elif self.re_search("Passed", status):
                self.StepsPassed += 1
            # elif have_word("Not execut", status):
            elif self.re_search("Not executed", status):
                self.StepsNotExecuted += 1
            else:
                self.StepsIncomplete += 1
        if self.StepsPassed!=self.TotalSteps-len(self.skip_step):
            self.is_pass = False
        logger.info(f"""步数统计，Total:{self.TotalSteps}, 
                    Failed:{self.StepsFailed}, Passed:{self.StepsPassed}, 
                    not executed:{self.StepsNotExecuted}, incomplete:{self.StepsIncomplete}""")
    
    def check_actual_result(self):
        '''
            检查actual_result和status状态
        '''
        result_dict = {"As expected":"Passed", "Failed":"Failed", "Not executed":"Not executed"}
        for info in self.steps_info:
            cells = info['row'].cells
            step_num = info['step_num']
            pre = f'step{step_num} '
           
            map_result, map_status = None, None
            # filter_result：Actual Result有没有写As expected|Failed|Not executed
            # 返回result_dict的键，与报告里实际写的可能有差异，报告里写的可能有句号，可能大小写不一样
            filter_result = next((r for r in result_dict if have_word(r[:-2], info['ActualResult'])), False) # 截掉“ed”匹配
            if filter_result:
                map_status = result_dict[filter_result] # 返回result_dict的值

            # filter_status：Status有没有写Passed|Failed|Not executed
            # 返回result_dict的值，与报告里写的可能有差异，尤其是有的人“Passed”只写为“Pass”
            filter_status = next((result_dict[r] for r in result_dict if 
                                        self.re_search(result_dict[r], info["Status"])
                                  ), False
                                ) # 截掉“ed”匹配
            if filter_status:
                map_result = next((k for k,v in result_dict.items() if v==filter_status))
            # 有没有写证据
            filter_evidence = self.re_search("note_evidence", info['ActualResult'])

            # 1. 有test case edit无redmine
            if have_word("test case edit", info['ActualResult']) and not info['Redmines']:
                # 如果写了别的问题的redmine就区分不出来了
                yield CR.warnning(f"{pre}有TEST CASE EDIT，没写Redmine")

            # 2. Actual Result没写As expected|Failed|Not executed
            if not filter_result:
                if FIX and filter_status:
                    # 没写证据在最底部插入，写了证据在证据前面插入
                    index = -2 if filter_evidence else -1
                    self.insert_paragraph(index, map_result, cells[self.actual_result_index])
                yield CR.error(f"{pre}缺少{[i for i in result_dict]}", fix=filter_status)
            # 3. Status不是Passed|Failed|Not executed
            status_err = f"{pre}Status “{info['Status']}” "
            if not filter_status: 
                err = f"{status_err}未填写" if not info['Status'] else f"{status_err}填写错误"
                if FIX and filter_result:
                    self.set_content(map_status, cells[self.status_index])
                    info["Status"] = map_status
                yield CR.error(err, filter_result)
            # 4. Status未加ed
            elif "ed" not in info["Status"]:
                if '--sw' not in sys.argv:
                    yield CR.info(f"{status_err}没有加“ed”")
            # 5. 非Passed的步骤无redmine
            if filter_status!='Passed' and not info['Redmines']:
                yield CR.warnning(f"{status_err}没写Redmine")

            # 6. Actual result与Status不对应
            if filter_result and filter_status and map_status!=result_dict[filter_result]:
                yield CR.error(f"{pre} “{filter_result}”的Status应为“{map_status}”，而非“{filter_status}”")
    
    def check_evidence(self):
        for info in self.steps_info:
            cells = info['row'].cells
            step_num = info['step_num']
            pre = f'step{step_num} '
            # 是否写证据路径
            filter_evidence = self.re_search("note_evidence", info['ActualResult'])
            # 是否保存有证据
            evidence_path = None 
            if self.steps_dir:
                evidence_path = find_file(f'step.*?(?<!\\d){step_num}(?!\\d)', self.steps_dir) 
                # 解决step4~6, 匹配不到步骤5的问题
                if not evidence_path:
                    for file in os.listdir(self.steps_dir):
                        path = os.path.join(self.steps_dir, file)
                        match = re.search(r'step[^\w]*?(\d+)[^\w]*?~(\d+)', path)
                        if match and int(step_num)>=int(match.group(1)) and int(step_num)<=int(match.group(2)):
                            evidence_path = path
                
            logger.info(f'{pre}的证据路径是{evidence_path};')
            if evidence_path: 
                steps = re.findall(r"(\d+)", os.path.basename(evidence_path)) # 文件夹名字可能是step1#2#3#
                # 解决证据路径是否跨越多个步骤，如step4~6，匹配不到步骤5的问题
                is_merge_step = re.search(r'step[^\w]*?\d+[^\w]*?~\d+', evidence_path)
                if is_merge_step:
                    steps = [i for i in range(int(steps[0]), int(steps[1])+1)]

                # 合并所有证据文件夹步骤（如step1#2#3#）的结果
                total_actual_result = ''.join(self.steps_info[self.steps_info_ix[str(i)]]['ActualResult'] for i in steps)

                # 20240401修改：有的人单个证据文件直接以stepxx命名，而不放进stepxx文件夹
                if OS.isdir(evidence_path) and OSis_empty(evidence_path):
                    yield CR.error(f"{pre}证据文件夹“{OS.basename(evidence_path)}”为空")
                else:
                    # 是否引用证据文件
                    if OS.isfile(evidence_path):
                        files = [OS.basename(evidence_path)]
                        evidence_path = OS.dirname(evidence_path)
                    else:
                        files = os.listdir(evidence_path)
                    for file in files:
                        # if not OSis_hidden(file) and file not in info['ActualResult']: # 屏蔽隐藏文件
                        if not OSis_hidden(file) and file not in total_actual_result: # 屏蔽隐藏文件
                            yield CR.info(f"{pre}未引用证据文件{file}")
                    # 证据路径是否填写正确
                    correct_path = evidence_path.replace(OS.dirname(self.notebook_dir), '').strip(os.sep)
                    if filter_evidence is None: # 未填写
                        if FIX: 
                            self.insert_paragraph(-1, 
                                f"\nNote: Pls find the evidence from path: {correct_path}", cells[self.actual_result_index])
                        yield CR.error(f"{pre} 有证据文件，但未写...Pls find evidence...", FIX)
                    else: # 填写错误 
                        note_evidence_value = filter_evidence.group(1).strip().strip('.').strip().strip(os.sep) # -_-!
                        pure = PurePath(evidence_path)
                        if not pure.match(note_evidence_value): # 证据路径写的不对
                            e = f"{pre}evidence path 填写不正确，“{note_evidence_value}”应该为“{correct_path}”"
                            if FIX:
                                self.replace_para_content(note_evidence_value, correct_path, cells[self.actual_result_index])
                            yield CR.warnning(e, FIX)
            elif filter_evidence: # 证据文件不存在+报告里写了证据
                e = f"{pre}找不到证据文件夹“{self.parent.case_name} --> {pre}”，但执行结果里写...Pls find evidence..."
                if FIX:
                    note_evidence_value = filter_evidence.group() 
                    self.delete_para_content(note_evidence_value, cells[self.actual_result_index])
                yield CR.error(e, FIX)
        # for e in self.check_evidence_file_time():
        #     yield e

    def check_step(self):
        for err in self.filter_step_row():
            yield err
        self.get_step_info()
        self.count_pass_fail()
        for err in self.check_actual_result():
            yield err
        if '--sw' not in sys.argv:
            for err in self.check_evidence():
                yield err
        for err in self.check_evidence_file_time():
            yield err
    
    
    def get_video_capture_time(self, file_path):
        return 
        # todo 获取视频拍摄时间的方法
        # import exifread
        # with open(file_path, 'rb') as f:
        #     tags = exifread.process_file(f)
        #     capture_time = tags['EXIF DateTimeOriginal']
        #     return capture_time.printable


    def get_photo_time(self, photo_path):
        try:
            if re.search('.mp4$', photo_path, flags=re.I) is not None:
                value = self.get_video_capture_time(photo_path)
                # print(photo_path, "视频时间为", value)
            else:
                from PIL import Image
                from PIL.ExifTags import TAGS
                import piexif
                import random
                from datetime import datetime
                image = Image.open(photo_path)
                exif_data = image._getexif()
                for tag, value in exif_data.items():
                    tag_name = TAGS.get(tag)
                    if tag_name == 'DateTimeOriginal':      
                        photo_time_fmt = "%Y:%m:%d %H:%M:%S"
                        value = datetime.strptime(value, photo_time_fmt).timestamp() # -13*3600 # 20240812修改为以美国纽约时间为准
                        return value
        except:
            # traceback.print_exc()
            pass

    def check_evidence_file_time(self):
        if self.steps_dir:
            for cur, dirs, files in os.walk(self.steps_dir):
                for file in files:
                    f_path = os.path.join(cur, file)
                    file_t = self.get_photo_time(f_path) # 20240812修改，优先获取拍摄时间
                    if not file_t:
                        try:
                            file_t = os.path.getmtime(f_path) 
                        except:
                            file_t = os.path.getmtime("\\\\?\\"+f_path)  # 解决路径过长问题
                    f_modify_dt_raw = dt.fromtimestamp(file_t) 
                    f_modify_dt = f_modify_dt_raw.replace(hour=0, minute=0, second=0)
                    # print(f_path, f_modify_dt)
                    if self._max_time is None or f_modify_dt-self._max_time>=timedelta(days=1): # 文件修改时间 大于 报告测试时间范围的最大值
                        yield CR.error(f"证据文件{f_path.replace(OSparent(f_path, 4), '')}的修改时间{f_modify_dt_raw}大于报告测试时间范围最大值{self._max_time}")
                    if self._min_time is None or f_modify_dt-self._min_time<=timedelta(days=-1): # 文件修改时间 大于 报告测试时间范围的最大值
                        # print(f_modify_dt-self._min_time)
                        yield CR.error(f"证据文件{f_path.replace(OSparent(f_path, 4), '')}的修改时间{f_modify_dt_raw}小于报告测试时间范围最小值{self._min_time}")
        else:
            yield CR.info("找不到证据文件夹")

    def check_zip_time(self, zip_path):
        """
            1、判断压缩包时间是否大于所有证据文件时间
            2、报告测试时间最大值是否大于所有证据文件修改时间
        """
        zip_modify = os.path.getmtime(zip_path) 

        # f_modify, f_name = 0, ""
        for cur, dirs, files in os.walk(self.steps_dir):
            for file in files:
                f_path = os.path.join(cur, file)
                file_t = os.path.getmtime(f_path) 
                if file_t - zip_modify >3:
                    yield CR.error(f"证据文件{f_path.replace(self.notebook_dir, '')}的修改时间大于压缩文件修改时间")
        zip_modify_dt = dt.fromtimestamp(zip_modify)
        # if zip_modify_dt - self._max_time >= timedelta(days=1):
        #     yield f"测试时间没有包含证据压缩文件时间"


    def check_md5(self):
        '''
        1. 有没有写md5
        2. 有没有压缩文件
        '''
        record_md5_re = self.re_search('MD5', self.record)
        if record_md5_re:
            record_md5 = record_md5_re.group(1).strip()
        else:
            record_md5 = ''
            yield CR.error('未填写MD5， 或填写不正确') # 这里等到下面判断zip文件存在之后再FIX
        # 获取证据压缩文件
        e_zip = self.steps_dir+".zip" if self.steps_dir else ''
        logger.info(f"{self.SystemID}证据压缩文件路径:{e_zip};")
        if not os.path.exists(e_zip):
            e = f'找不到证据压缩文件{e_zip}' 
            fix = bool(FIX and e_zip and '--zip' in sys.argv)
            if fix: 
                e_zip = shutil.make_archive(self.steps_dir, "zip", 
                                   root_dir=os.path.dirname(self.steps_dir),
                                   base_dir=os.path.basename(self.steps_dir)
                                   )
                e += '已自动压缩 '
            yield CR.error(e, fix)
        if os.path.exists(e_zip): # 可能上面自动压缩，所以要再次判断
            zip_md5 = calculate_md5(e_zip)
            if record_md5!=zip_md5: # 包含record_md5填写和未填写两种情况
                e_zip_name = e_zip.replace(OSparent(e_zip,2), "")
                if FIX:
                    cell = self.rows[self.end_step_ix+1].cells[0]
                    # 修改md5值
                    if not record_md5:
                        self.insert_paragraph(-1, 'MD5:'+zip_md5, cell)
                    else:
                        self.replace_para_content(record_md5, 'MD5:'+zip_md5, cell)
                yield CR.error(f'填写的MD5:{record_md5}与文件{e_zip_name}的md5:{zip_md5}不一致', True)
            for e in self.check_zip_time(e_zip):
                yield e
        logger.info("check MD5结束") # MD5检查比较耗时

    def check_record(self):
        '''
            1. 检查时间
            2. 检查failed步骤有没有说明
            3. 检查MD5
        '''
        if not self.re_search("TestTime", self.record):
            yield CR.error("Test Record没写测试时间")
        for info in self.steps_info:
            if not self.re_search("Passed", info['Status']) and \
                not have_word(f'step.*?(?<!\\d){info["step_num"]}(?!\\d)', self.record):
                yield CR.warnning(f"Test Record未对Status为“{info['Status']}”的step{info['step_num']}说明")
        for e in self.check_md5():
            yield e

    def check_Conclusion(self):
        '''
            如果有failed的步骤但是结论是pass则给出提示；如果步骤都pass结论是错的则报错
        '''
        logger.info("check conclusion开始")
        err = f"Conclusion “{self.Conclusion}” "
        is_pass = self.re_search("Passed", self.Conclusion) 
        is_fail = self.re_search("Failed", self.Conclusion)
        if not self.Conclusion or (not is_pass and not is_fail):
            yield CR.error(f"{err}填写不正确, 建议写“Passed”或者“Failed”")
        elif 'ed' not in self.Conclusion:
            if '--sw' not in sys.argv:
                yield CR.info(f"{err}没有加“ed”")

        if is_pass and self.StepsPassed!=(self.TotalSteps-len(self.skip_step)):
            yield CR.error(f"{err}，但有步骤Failed或Not executed")
        if is_fail and self.StepsPassed==(self.TotalSteps-len(self.skip_step)):
            yield CR.error(f"{err}，但所有步骤都Passed")
            # todo FIX
        logger.info("check conclusion结束")

    def check_equipment(self):
        '''
            1. 检查设备编码
            2. 检查证书
        '''
        if not have_word('N/?A', self.equipment):
            equipments = self.re_search('equipment', self.equipment, find_all=True)
            logger.info(f"设备编码：{equipments};")
            if not equipments:
                yield CR.warnning(f"equipments未填写或者未写设备编码")
            else:
                for e in equipments:
                    files = []
                    if self.parent.evidence_dir:
                        files += os.listdir(self.parent.evidence_dir)
                    if self.steps_dir:
                        files += os.listdir(self.steps_dir) + os.listdir(OSparent(self.steps_dir)) 
                    has_certification = [file for file in files if e in file and '.pdf' in file]
                    if not has_certification:
                        yield CR.error(f"找不到{e}的校准证书，确认证书名字是否包含设备编码")

    def check(self):
        g_list = [
            self.check_title(),
            self.check_step(),
            self.check_record(),
            self.check_Conclusion(),
            self.check_equipment(),
        ]
        for g in g_list:
            for err in g:
                yield err
    
    @property
    def serializer(self):
        _max_time = dt.strftime(self._max_time, format='%d-%b-%Y') if self._max_time else None
        _min_time = dt.strftime(self._min_time, format='%d-%b-%Y') if self._min_time else None
        extra_dict = {
            "_max_time": _max_time,
            "_min_time": _min_time,
        }
        return Serializer(self, extra_dict=extra_dict)


class MergeTable:
    def __init__(self, ts, parent=None):
        '''
            模拟python-docx的Table虚拟类
        '''
        self.__ts = ts
        self.rows = []
        i = 0
        for t in ts:
            for r in t.rows:
                # 改变后面表格的行序号
                # 本来想直接改变r._index的值，但是因为这个是python-docx Table的property属性，没办法直接修改，所以用index代替
                # 上面的ExecutionTable也要把_index 替换成 index
                r.index = i 
                self.rows.append(r)
                i += 1

    def cell(self, row, col):
        return self.rows[row].cells[col]
    
            
class ExecutionTableGroup:
    def __init__(self, tables, parent=None):
        '''
            execution表格由多个表格组成
            目标：
                将多个表格组合成一个table对象，这个table对象要支持如下属性：
                    self.table.rows,
                还要支持如下方法：
                    self.table.cell(row, column)
                    #todo 实现HmDocx的cp_table方法
                    #todo 实现HmTable的insert_row方法
        '''
        # 3. 将虚拟表格类实例传递给ExecutionTable类的table参数
        # 4. todo 重写ExecutionTable实例的cp_table方法和insert_row方法
        self.tables = tables
        self.parent = parent
        self.t_ix = [] # 每个group表的第一个表格索引
        self.exetbl_groups = self.group_exe()

        if self.exetbl_groups:
            self.is_me = True 
        else: 
            self.is_me = False
        
    def group_exe(self):
        '''
            查找一共有多少个execution
            顺序遍历每个表格，如果满足target_conditions，认为是第一个execution表格，直到下一个execution表格之间的所有表为一个group
            return: [[table1, table2], [table3, table4]] # 每个子列表是一个execution
        '''
        exetbl_groups = []
        group = []
        start_append = False
        for i,t in enumerate(self.tables):
            # 查找执行表格 开头的标题
            target_conditions = ["Tester", "Version", "Date.*Tested", "Platform"] 
            find_conditions = []
            cells = [cell for r in t.rows for cell in r.cells]
            for cell in cells:
                for condition in target_conditions:
                    if have_word(condition, cell.text):
                        if condition not in find_conditions:
                            find_conditions.append(condition)
                        break
                if len(find_conditions)==len(target_conditions):
                    start_append = True
                    self.t_ix.append(i)
                    if group:
                        exetbl_groups.append(group)
                        group = []
                    break
            if start_append:
                group.append(t)
        if start_append and group:
            exetbl_groups.append(group)
        return exetbl_groups

    def get_execution_tables(self):
        '''
            生成ExecutionTable实例对象组成的列表
        '''
        execution_tables = []
        for eg in self.exetbl_groups:
            if len(eg)>1:
                t = MergeTable(eg) 
            else:
                t = eg[0]
            e = ExecutionTable(t, self.parent)
            execution_tables.append(e)
        return execution_tables
        

class Execution(NotebookDocx):
    def __init__(self, path, *args, **kwargs):
        super().__init__(path)
        self.eg = ExecutionTableGroup(self.tables, self)
        self.is_me = self.eg.is_me
        if self.is_me:
            self.setup(*args)
        
    def setup(self, *args):
        self.re_search = ReSearchWrapper("Execution")
        self.s = None # Summary实例化对象
        self.init_dir()
        self.execution_tables = self.get_exe_tbl()
        self.is_retested = self.retested_report()
        self.chapters = self.get_chapters()
        self.chapter = ''
        if self.chapters:
            self.chapter = self.chapters[0]
        logger.info(f"chapter is:{self.chapter}")
        
    def get_chapters(self):
        '''
            获取用例章节
        '''
        chapters = []
        chapter = ''
        t_ix = 0
        for i,x in enumerate(self.iter_block_items(self.doc)):
            if isinstance(x, Paragraph) and x.text:
                chapter = x.text
            if isinstance(x, Table):
                if t_ix in self.eg.t_ix:
                    chapters.append(chapter)
                t_ix += 1
        return chapters

    
    def get_exe_tbl(self):
        return self.eg.get_execution_tables()
    
    # def get_exe_tbl(self):
    #     for i,t in enumerate(self.tables):
    #         try:
    #             # 20240619 适配SW格式
    #             if '--sw' in sys.argv and i+2<len(self.tables):
    #                 e = ExecutionTable([t, self.tables[i+1], self.tables[i+2]], self)
    #                 self.execution_tables.append(e)
    #                 # break
    #             else:
    #                 e = ExecutionTable(t, self)
    #                 self.execution_tables.append(e)
    #         except AssertionError:
    #             logger.warning(f"不是执行表格")
    #         except:
    #             logger.error(f'初始化执行表格出错:\n{traceback.format_exc()}')
    #             raise Exception("初始化执行表格错误, 检查表格最后一列是否对齐，检查表格格式是否正确")
    
    def init_dir(self):
        '''
            self.case_name有两个用途:
                1. 用于执行表格查找证据路径
                2. 用于查找总结文件路径
        '''
        match = self.re_search("case_name", self.filename)
        if match: # and self.summary_dir:
            self.case_name = match.group(1)
        else:
            self.case_name = OSname(self.filename)
        logger.info("++++++++++ execution self.case_name is "+self.case_name)
        # 20240401修改，不区分文件的"_"和"-"
        self.summary_path = find_file(self.case_name.replace("_", "[ _-]")+".doc", self.summary_dir, only_file=True)
    
    def retested_report(self):
        """
            判断是否为重新执行的文件
        """
        files = [i for i in os.listdir(OS.dirname(self.path)) if i!=self.filename]
        for file in files:
            # 如果有文件名加了“- xxx”后缀，等于self.filename
            pattern = "^"+OSname(file)+r" *?[_-]((?!(output)).)+"+OSext(file)+"$"
            if re.search(pattern, self.filename):
                logger.info(f"{self.filename}是{file}的重测文件")
                return True
        return False
        
    def check(self):
        yield f"\n\n{Fore.LIGHTGREEN_EX}检查文件 {self.filename}"
        for err in super().check():
            yield err
        for i, t in enumerate(self.execution_tables):
            yield f"    {Fore.YELLOW}检查表格{i+1} ({t.SystemID})"
            num = 0
            for err in t.check():
                num += 1
                yield f"        {Fore.LIGHTCYAN_EX}{num: >3}{Fore.RESET}、{err}"
        
    @property
    def serializer(self):
        s = self.s.serializer if self.s else None
        extra_dict = {
            "execution_tables": [t.serializer for t in self.execution_tables], 
            "s": s,
        }
        ser = Serializer(self, extra_dict=extra_dict)
        return ser

    
class SummaryTable(HmTable):
    # HmTable 的ATTR是从这里演变过去的
    cell_info_dict = {
                "TestCaseNO":{"key":"Test Case", "row_index":0, "cell_index":0},
                "Tester":{"key":"Tested by", "row_index":-1, "cell_index":0},
                "SystemID":{"key":"Analyzer ID", "row_index":1, "cell_index":0},
                "assay_data_disk":{"key":"Assay Data Disk", "row_index":1, "cell_index":None},
                "SoftwareVersion":{"key":"Software Version", "row_index":2, "cell_index":0},
                "TimeSpent":{"key":"Time Spent", "row_index":2, "cell_index":None},
                "StartDate":{"key":"Start Date", "row_index":4, "cell_index":0},
                "EndDate":{"key":"End Date", "row_index":4, "cell_index":None},
                "StepsPassed":{"key":"Steps Passed", "row_index":5, "cell_index":None},
                "StepsFailed":{"key":"Steps Failed", "row_index":5, "cell_index":None},
                "StepsIncomplete":{"key":"Steps Incomplete", "row_index":5, "cell_index":None},
                "StepsNotExecuted":{"key":"Steps Not Executed", "row_index":5, "cell_index":None},
                "TotalSteps":{"key":"Total Steps", "row_index":6, "cell_index":0},
                "Conclusion":{"key":"Conclusion", "row_index":-4, "cell_index":0},
                "ConclusionRationale":{"key":"Conclusion Rationale", "row_index":None, "cell_index":0},
                "Limitations": {"key":"Limitations", "row_index":-2, "cell_index":0},
            }
 
    def __init__(self, table, parent):
        super().__init__(table, parent)
        # assert self.is_me, f'{self.table}不是summary表格'
        self.is_me = self._is_me()
        if self.is_me:
            self.setup(table, parent)   
            
    def _is_me(self):
        try:
            cells = self.rows[0].cells
            end_cells = self.rows[-1].cells
            if have_word('Test Case',cells[0].text) and have_word("Tested by", end_cells[0].text):
                return True
        except:
            logger.warning(f"调用SummaryTable is_me方法错误：{traceback.format_exc()}")
        return False
     
    def setup(self, table, parent):    
        logger.info("********** [初始化summary报告表格] **********")
        # 这样做行不通，暂时找不到原因
        # for key iinself.cell_info_dict:
        #     get_fun = lambda self: self.get_info(self.cell_info_dict[key]["key"], 
        #         self.cell_info_dict[key]["row_index"], self.cell_info_dict[key]["cell_index"])
        #     set_fun = lambda self, value: self.set_info(self.cell_info_dict[key]["key"], value, 
        #         self.cell_info_dict[key]["row_index"], self.cell_info_dict[key]["cell_index"])
        #     attr_value = property(get_fun, set_fun)
        #     setattr(SummaryTable, key, attr_value)
    

        # 表格单元格的内容转为这个类的实例属性
        for key in self.cell_info_dict:
            # 注意setattr(self,)也会调用self.__setattr__
            setattr(self, key, self.get_info(self.cell_info_dict[key]["key"], 
                self.cell_info_dict[key]["row_index"], self.cell_info_dict[key]["cell_index"]))
        # 格式化self.TimeSpent的值，以便和执行文件统一格式进行对比
        r = re.search(r"[\d\.]+", self.TimeSpent)
        if r:
            unit = ' Hours' if r.group()!='1' else ' Hour'
            if self.TimeSpent != r.group() + unit:
                self.TimeSpent = r.group() + unit 
        
        # self.etable # 对应的执行表格对象, 改为property，保证每次sumary表格内容修改后，还能获取到正确的执行表格
        self.start_step_row, self.end_step_row = self.start_end_step_row()
        self.start_step_ix, self.end_step_ix = self.start_step_row._index, self.end_step_row._index
        self.status_descrip_i = self.locate_cell('Status Description', row=self.start_step_ix-1)[1] 
        self.step_status_i = self.locate_cell('Step Status', row=self.start_step_ix-1)[1] # step_status_index
        self.steps_info = [] # 所有步骤信息，在check_step之后生效
        self.auto_summary = False
        if "--auto_summary" in sys.argv:
            self.auto_summary = True

        
        for key in self.cell_info_dict:
            logger.info(f"{key}:{getattr(self, key)};")
        logger.info(f'''TimeSpent:{self.TimeSpent};
            status_descrip_i:{self.status_descrip_i};
            step_status_i:{self.step_status_i};
        ''')
        
    def __setattr__(self, key, value):
        '''
            如果访问的属性属于单元格信息，则操作单元格
        '''
        if key in self.cell_info_dict: 
            if hasattr(self, key) and getattr(self, key)!=value: # 内容不一样再修改
                ck = self.cell_info_dict[key]
                self.set_info(ck["key"], value, ck["row_index"], ck["cell_index"])
        super().__setattr__(key, value)
    
    def get_info(self, key, row=None, cell_index=None):
        '''
            获取summay表格的信息，如Analyzer ID, Software Version等
        '''
        regex = f"^{re.escape(key)}: *(.*)"
        target_cell = self.locate_cell(regex, re.I|re.DOTALL, row=row, col= cell_index)
        if target_cell and target_cell[2]:
            if target_cell[2].groups():
                value = target_cell[2].group(1)
            else:
                value = target_cell[2].group()
            return value.strip()
    
    def set_info(self, key, info, row=None, cell_index=None):
        '''
            设置单元格信息
        '''
        regex = f"^{re.escape(key)}: *"
        target_cell = self.locate_cell(regex, row=row, col=cell_index)
        if target_cell and info:
            cell = self.rows[target_cell[0]].cells[target_cell[1]]
            new_info = key+": "+ info
            self.set_content(new_info, cell) # 操作单元格
       
    # @property
    # def TestCaseNO(self):
    #     '''
    #         通过property实现访问此对象属性即查看单元格信息，和设置属性的值即修改单元格信息，
    #         但是这样做需要重复写很多代码，已改为通过setattr和__setattr__实现
    #     '''
    #     return self.get_info("Test Case", self.rows[0], 0)

    # @TestCaseNO.setter
    # def TestCaseNO(self, value):
    #     return self.set_info("Test Case", value, 0)
    
    def start_end_step_row(self):
        '''
        # 获取第1步和最后1步执行步骤的行索引
        '''
        start, end = None, None
        for i, row in enumerate(self.rows):
            if have_word("Step", row.cells[0].text) and have_word("Status Description", row.cells[2].text):
                # start = i+1
                start = self.rows[i+1]
            if have_word("Conclusion:", row.cells[0].text):
                # end = i-1
                end = self.rows[i-1]
        if start is None or end is None:
            raise Exception("找不到Summary测试step定位行，检查step标题行和Conclusion行是否存在")
        logger.info(f"Summary start, end:[{start},{end}]")
        return start, end
    
    @property
    def etable(self):
        '''
        查找对应的执行表格
        '''
        for etable in self.parent.execution.execution_tables:
            if etable.SystemID == self.SystemID:
                return etable
    
    def check_info(self):
        for c in self.cell_info_dict:
            myvalue = str(getattr(self, c)).strip()
            key = self.cell_info_dict[c]["key"]
            if c=="ConclusionRationale" or c=="Limitations":
                if not myvalue:
                    yield CR.error(f"未填写{key}")
                    if FIX:
                        if self.StepsPassed == self.TotalSteps:
                            new_value = 'All steps executed Passed.'
                        else:
                            new_value = f"{self.StepsPassed} executed Passed, {self.StepsFailed} executed Failed."
                        setattr(self, c, new_value)
                continue
            evalue = str(getattr(self.etable, c))
            if not myvalue:
                if FIX and evalue:
                    setattr(self, c, evalue)
                yield CR.error(f"未填写{key}", evalue)

            elif not evalue:
                yield CR.error(f"执行报告里没有{key}")
            elif myvalue!=evalue:
                if evalue == 'None':
                    evalue = ''
                if c=='TimeSpent' and not evalue: # 20240426修改，不再检查时间
                    continue
                e = CR.error(f"Summary的{key}为“{myvalue}”，与执行报告的“{evalue}”不一样")
                if FIX and evalue:
                    setattr(self, c, evalue)
                yield CR.error(e, evalue)
    
    def get_estep_info(self, step):
        '''
            step:字符串
        '''
        if step in self.etable.steps_info_ix:
            return self.etable.steps_info[self.etable.steps_info_ix[step]]
        elif step not in self.etable.skip_step:
            return CR.error(f'在执行文件找不到step{step}')

    def check_un_sum_steps(self, un_sum_steps):
        '''
            检查已写的步骤
        '''
        rows = self.rows[self.start_step_ix:self.end_step_ix+1]
        for i, row in enumerate(rows):
            cells = row.cells
            step_text = cells[0].text.strip()
            status_description = cells[self.status_descrip_i].text.strip()
            step_status = cells[self.step_status_i].text.strip()
            step_nums = re.findall(r'\d+', step_text)
            self.steps_info.append({"step_nums":step_nums, 
                                    "status_description":status_description,
                                    "step_status":step_status})
            
            # todo 解决 如果写的是23~26， 则忽略了25步骤问题


            # 解决N/A行仍然检查的问题
            if have_word("n.?a", step_text):
                continue
            # 1. 检查Step#列是否有测试步骤
            if not step_nums and not have_word("N/?A", step_text):
                yield CR.error(f"步骤的第{i+1}行找不到有效的step number，也不是N/A")
            else:
                correct_status = 'Passed'
                for step in step_nums:
                    # e_step_info = self.etable.steps_info[int(step)-1] # 获取执行文件的step信息
                    # e_step_info = self.etable.steps_info[self.etable.steps_info_ix[step]]
                    e_step_info = self.get_estep_info(step)
                    if not e_step_info:
                        continue
                    if isinstance(e_step_info, str):
                        yield e_step_info
                        continue
                    e_Status = e_step_info['Status']


                    if step in un_sum_steps:
                        un_sum_steps.remove(step)
                    # 下面的代码本来在上面两行的if语句里，但如果报告里不同行出现同样的步数，则重复的步数就会跳过检查
                    # 为了解决这个问题，将下面代码移到了if之外
                    # 2. 检查Step Status列，有没有execution里的Redmine，TEST CASE EDIT
                    for redmine in e_step_info['Redmines']:
                        if not have_word(redmine, status_description):
                            yield CR.warnning(f"step{step} 缺少redmine:{redmine}")
                    if e_step_info["TEST_CASE_EDIT"]:
                        if not have_word("TEST CASE EDIT", status_description):
                            yield CR.warnning(f"step{step} 缺少TEST CASE EDIT")
                    # # 3. 检查Step Status和execution里的Status是否一致
                    # if step_status!=e_Status:
                    #     if FIX:
                    #         target_cell = cells[self.step_status_i]
                    #         self.set_content(e_Status, target_cell)
                    #     yield CR.error(f"step{step} 的Step Status为{step_status}，与执行报告的{e_Status}不一样", FIX)
                    
                    # 解决同一行包含多个步骤，status判断错误的问题
                    if not have_word(e_Status, correct_status) and not have_word("pass", e_Status):
                        correct_status = e_Status
                        

                # 解决同一行包含多个步骤，status判断错误的问题
                # 3. 检查Step Status和execution里的Status是否一致
                if step_status!=correct_status and step_status+"ed"!=correct_status:
                    if FIX:
                        target_cell = cells[self.step_status_i]
                        self.set_content(correct_status, target_cell)
                    yield CR.error(f"{step_text} 的Step Status为{step_status}，与执行报告的{correct_status}不一样", FIX)
        yield un_sum_steps

    def add_step_index(self, step):
        """
            查找execution里应该总结的但是Summary没写的步骤
            应该插入到Summary表格的哪一行（相对于self.start_step_ix）
        """
        i = -1 # self.steps_info可能为空
        for i, info in enumerate(self.steps_info):
            for n in info["step_nums"]:
                if n.isdigit() and int(n)>int(step): # 如果当前步骤No大于要插入的步骤No
                    return i # 把要插入的步骤，插入到当前步骤所在的行（当前步骤变为下一行）
        return i+1 # 上面的步骤都不满足，插入到最后一步

    def add_sum_step(self, un_sum_steps):
        '''
            检查并添加未写的步骤
        '''
        for i, step in enumerate(un_sum_steps):
            # e_step_info = self.etable.steps_info[int(step)-1]
            # e_step_info = self.etable.steps_info[self.etable.steps_info_ix[step]]
            e_step_info = self.get_estep_info(step)
            if not e_step_info:
                continue
            if isinstance(e_step_info, str):
                yield e_step_info
                continue
            e = None
            s = f"执行报告的step{e_step_info['step_num']} "
            if e_step_info["Redmines"]:
                e = s + f"有Redmine：{e_step_info['Redmines']}，"
            if e_step_info["TEST_CASE_EDIT"]:
                e = s + f"有TEST CASE EDIT：{e_step_info['TEST_CASE_EDIT']}，"
            if not have_word("Pass", e_step_info["Status"]):
                e = s + f"Status为“{e_step_info['Status']}”，"
            if e:
                e += "但未总结"
                if FIX and self.auto_summary:
                    # 插入新行
                    ix = self.add_step_index(step)
                    row = self.insert_row(self.start_step_ix + ix)
                    step_num = e_step_info['step_num']
                    ActualResult = e_step_info['ActualResult']
                    Status = e_step_info['Status']
                    self.set_content(f"step{step_num}", row.cells[0] )
                    self.set_content(ActualResult, row.cells[self.status_descrip_i] )
                    self.set_content(Status, row.cells[self.step_status_i] )
                    # 更新steps_info 否则下一步插入位置不正确
                    # 因为add_step_index函数的回值取决于self.steps_info
                    self.steps_info.insert(ix, {"step_nums":step_num, 
                                        "status_description":ActualResult,
                                        "step_status":Status})
                    self.end_step_ix = self.end_step_row._index # 这个虽然不影响结果但是也更新一下
                yield CR.warnning(e, self.auto_summary)

    def check_step(self):
        '''
            满足以下条件的步骤，需要sumary
                1） 结果为failed/Not executed
                2） 有redmine
                3） 有TEST CASE EDIT
        '''
        # self.etable.steps_info值的格式：
        # [{"step_num": None, "ActualResult":None, "Status":None, "Redmines":[], "TEST_CASE_EDIT":None},...]
        # 应该总结的步骤
        un_sum_steps = [step["step_num"] for step in self.etable.steps_info 
                            if not have_word("Pass", step["Status"]) or 
                                step["Redmines"] or step["TEST_CASE_EDIT"]]
        logger.info(f"un_sum_steps:{un_sum_steps};")
        for e in self.check_un_sum_steps(un_sum_steps):
            if id(e)!=id(un_sum_steps):
                yield e
            elif un_sum_steps:
                logger.info(f"add_sum_step:{un_sum_steps};")
                for e in self.add_sum_step(un_sum_steps):
                    yield e
                    
    def check(self):
        '''
            1. 与执行表格的信息对比
            2. 与执行表格的步骤对比
            3. 不符合的地方自动FIX
        '''    
        if self.etable:
            g_list = [
                self.check_info(),
                self.check_step(),
            ]
            for g in g_list:
                for err in g:
                    yield err
        else:
            yield CR.error(f"执行文件里找不到SystemID为“{self.SystemID}”的表格")

    @property
    def serializer(self):
        serializer = Serializer(self)
        return serializer


class Summary(NotebookDocx):
    def __init__(self, path, *args, **kwargs):
        '''
            execution 是执行文件实例化对象，之所以可以为None
                是因为当path不确定是否为sumary文件时，可以先实例化,然后通过is_me方法来判断
            注意，调用过check方法的execution对象才能得到完整的执行文件信息
        '''
        super().__init__(path)
        self.re_search = ReSearchWrapper("Summary")
        if self.is_me:
            self.setup(*args)
    
    def _is_me(self):
        try:
            rows = self.tables[0].rows
            cells = rows[0].cells
            end_cells = rows[-1].cells
            if have_word('Test Case',cells[0].text) and have_word("Tested by", end_cells[0].text):
                return True
        except:
            logger.warning(f"调用Summary is_me方法错误：{traceback.format_exc()}, 文件路径是{self.path}")
        return False

    def setup(self, *args):
        self.execution = args[0]
        self.init_dir()
        if self.execution:
            assert isinstance(self.execution, Execution), "应传入执行文件对象"
        self.summary_tables = []
        for t in self.tables:
            st = SummaryTable(t, self)
            if st.is_me:
                self.summary_tables.append(st)
            
            # try:
            # except:
            #     logger.error(f'初始化Summary表格出错:\n{traceback.format_exc()}')
            #     raise Exception(f"初始化Summary表格错误，检查表格最后一列是否对齐， 检查表格格式是否正确")
            
    @classmethod
    def create_summary(cls, execution):
        '''
            生成execution对应的Summary文件（直接复制模板文件）
        '''
        if execution.case_name:
            file_name = f"TestSummaryReport-{execution.case_name}.docx"
        else:
            file_name = OSname(execution.filename)
        summary_dir = execution.summary_dir
        if not summary_dir:
            summary_dir = os.path.join(OSparent(execution.path, 2), "1-Test Summary")
            os.makedirs(summary_dir, exist_ok=True)
        path = os.path.join(summary_dir, file_name)
        template = os.path.join(os.path.dirname(__file__),"res","TestSummaryReport-template.docx")
        shutil.copy2(template, path)
        
        doc = cls(path, execution)
        for i,etable in enumerate(execution.execution_tables):
            # 修改模板文件第一个表格的systemID
            if i==0:
                t = doc.summary_tables[0]
                t.SystemID = etable.SystemID
                t.auto_summary = True
            # 模板文件只有一个summary表格（软件测试只需要一台仪器）
            # 如果还有需要总结的仪器，创建新表格
            else:
                t = doc.summary_tables[i-1].table # 前一个表格
                t = doc.cp_table(t) # 复制前一个表格
                t = SummaryTable(t, doc) 
                t.SystemID = etable.SystemID
                t.auto_summary = True
                doc.summary_tables.append(t)
        for e in doc.check():
            pass # FIX为真才会调用creat方法
        doc.save(True)
        return doc
    
    def init_dir(self):
        match = self.re_search("case_name", self.filename)
        if match: # and self.summary_dir:
            self.case_name = match.group(1)
        else:
            self.case_name = OSname(self.filename)
        logger.info("++++++++++summary self.case_name is "+self.case_name)
        # 20240401修改，不区分文件的"_"和"-"
        self.execution_path = find_file(self.case_name.replace("_", "[_-]")+".doc", self.execution_dir, only_file=True)
        logger.info(f"case_name:{self.case_name}; \nexecution_path:{self.execution_path}")
    
    def check_sum_tb_cnt(self):
        ids = [i.SystemID for i in self.summary_tables]
        for t in self.execution.execution_tables:
            if t.SystemID not in ids:
                yield CR.error(f"执行文件中SystemID为{t.SystemID}的表格未总结")
                # todo FIX
    
    def check(self):
        yield f"\n\n{Fore.LIGHTGREEN_EX}检查文件 {self.filename}"
        for err in super().check():
            yield err
        for i, t in enumerate(self.summary_tables):
            yield f"    {Fore.YELLOW}检查表格{i+1} ({t.SystemID})"
            num = 0
            for err in t.check():
                num += 1
                yield f"        {Fore.CYAN}{num: >3}、{Fore.RESET}{err}"
        for err in self.check_sum_tb_cnt():
            num += 1
            yield f"    {Fore.YELLOW}{err}"
    
    @property
    def serializer(self):
        extra_dict = {
            "summary_tables":[t.serializer for t in self.summary_tables],
        }
        return Serializer(self, extra_dict=extra_dict)
    


